/****************************************************************************
 * Copyright (c) 2009 Composent, Inc. and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *   Composent, Inc. - initial API and implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/
package org.eclipse.ecf.remoteservice.client;

import java.io.NotSerializableException;
import java.lang.reflect.Method;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.ecf.core.util.ECFException;
import org.eclipse.ecf.remoteservice.*;
import org.eclipse.ecf.remoteservice.events.IRemoteCallCompleteEvent;
import org.eclipse.ecf.remoteservice.events.IRemoteCallStartEvent;
import org.eclipse.equinox.concurrent.future.*;

/**
 * Remote service client service.  Implements {@link IRemoteService}.
 * 
 * @since 4.0
 */
public abstract class AbstractClientService extends AbstractRemoteService {

	private long nextID = 0;
	protected RemoteServiceClientRegistration registration;
	protected AbstractClientContainer container;

	public AbstractClientService(AbstractClientContainer container, RemoteServiceClientRegistration registration) {
		this.container = container;
		Assert.isNotNull(container);
		this.registration = registration;
		Assert.isNotNull(this.registration);
	}

	public Object callSync(IRemoteCall call) throws ECFException {
		IRemoteCallable callable = getRegistration().lookupCallable(call);
		if (callable == null)
			throw new ECFException("Callable not found for call=" + call); //$NON-NLS-1$
		return invokeRemoteCall(call, callable);
	}

	public IFuture callAsync(final IRemoteCall call) {
		return callAsync(call, getRegistration().lookupCallable(call));
	}

	public void callAsync(IRemoteCall call, IRemoteCallListener listener) {
		callAsync(call, getRegistration().lookupCallable(call), listener);
	}

	public void fireAsync(IRemoteCall call) throws ECFException {
		IRemoteCallable callable = getRegistration().lookupCallable(call);
		if (callable == null)
			throw new ECFException("Remote callable not found"); //$NON-NLS-1$
		callAsync(call, callable);
	}

	protected Object invokeSync(IRemoteCall remoteCall) throws ECFException {
		// Now lookup callable
		IRemoteCallable callable = getRegistration().lookupCallable(remoteCall);
		// If not found...we're finished
		if (callable == null)
			throw new ECFException("Callable not found for call=" + remoteCall); //$NON-NLS-1$
		return invokeRemoteCall(remoteCall, callable);

	}

	protected Object[] getCallParametersForProxyInvoke(String callMethod, Method proxyMethod, Object[] args) {
		return args;
	}

	protected String getCallMethodNameForProxyInvoke(Method method, Object[] args) {
		return RemoteServiceClientRegistration.getFQMethod(method.getDeclaringClass().getName(), method.getName());
	}

	protected long getNextRequestID() {
		return nextID++;
	}

	protected void callAsync(IRemoteCall call, IRemoteCallable restClientCallable, IRemoteCallListener listener) {
		final AbstractExecutor executor = new ThreadsExecutor();
		executor.execute(new AsyncResult(call, restClientCallable, listener), null);
	}

	protected IFuture callAsync(final IRemoteCall call, final IRemoteCallable callable) {
		final AbstractExecutor executor = new ThreadsExecutor();
		return executor.execute(new IProgressRunnable() {
			public Object run(IProgressMonitor monitor) throws Exception {
				if (callable == null)
					throw new ECFException("Callable not found"); //$NON-NLS-1$
				return invokeRemoteCall(call, callable);
			}
		}, null);
	}

	/**
	 * inner class implementing the asynchronous result object. This
	 * implementation also provides the calling infrastructure.
	 */
	protected class AsyncResult implements IProgressRunnable {

		IRemoteCall call;
		// the remote call object.
		IRemoteCallable callable;
		// the callback listener, if provided.
		IRemoteCallListener listener;

		// the result of the call.
		Object result;
		// the exception, if any happened during the call.
		Throwable exception;

		// constructor
		public AsyncResult(final IRemoteCall call, final IRemoteCallable callable, final IRemoteCallListener listener) {
			this.call = call;
			this.callable = callable;
			this.listener = listener;
		}

		public Object run(IProgressMonitor monitor) throws Exception {
			Object r = null;
			Throwable e = null;

			final long reqID = getNextRequestID();

			if (listener != null) {
				listener.handleEvent(new IRemoteCallStartEvent() {
					public IRemoteCall getCall() {
						return call;
					}

					public IRemoteServiceReference getReference() {
						return getRegistration().getReference();
					}

					public long getRequestId() {
						return reqID;
					}
				});
			}

			try {
				if (callable == null)
					throw new ECFException("Restcall not found for method=" + call.getMethod()); //$NON-NLS-1$
				r = invokeRemoteCall(call, callable);
			} catch (Throwable t) {
				e = t;
			}

			synchronized (AsyncResult.this) {
				result = r;
				exception = e;
				AsyncResult.this.notify();
			}

			if (listener != null) {
				listener.handleEvent(new IRemoteCallCompleteEvent() {

					public Throwable getException() {
						return exception;
					}

					public Object getResponse() {
						return result;
					}

					public boolean hadException() {
						return exception != null;
					}

					public long getRequestId() {
						return reqID;
					}
				});
			}
			return null;
		}
	}

	protected void handleInvokeException(String message, Throwable e) throws ECFException {
		throw new ECFException(message, e);
	}

	protected AbstractClientContainer getClientContainer() {
		return container;
	}

	protected RemoteServiceClientRegistration getRegistration() {
		return registration;
	}

	protected String prepareEndpointAddress(IRemoteCall call, IRemoteCallable callable) {
		return getClientContainer().prepareEndpointAddress(call, callable);
	}

	protected IRemoteCallParameter[] prepareParameters(String uri, IRemoteCall call, IRemoteCallable callable) throws NotSerializableException {
		return getClientContainer().prepareParameters(uri, call, callable);
	}

	/**
	 * @param uri uri
	 * @param call call
	 * @param callable callable
	 * @param responseHeaders responseHeaders
	 * @param responseBody responseBody
	 * @return Object processed response
	 * @throws NotSerializableException if response cannot be deserialized
	 * @since 8.0
	 */
	protected Object processResponse(String uri, IRemoteCall call, IRemoteCallable callable, Map responseHeaders, byte[] responseBody) throws NotSerializableException {
		return getClientContainer().processResponse(uri, call, callable, responseHeaders, responseBody);
	}

	protected IRemoteServiceID getRemoteServiceID() {
		return registration.getID();
	}

	protected IRemoteServiceReference getRemoteServiceReference() {
		return registration.getReference();
	}

	protected String[] getInterfaceClassNames() {
		return registration.getClazzes();
	}

	/**
	 * @since 8.5
	 */
	public static class UriRequest {
		private final String uri;
		private final IRemoteCall call;
		private final IRemoteCallable callable;

		public UriRequest(String uri, IRemoteCall call, IRemoteCallable callable) {
			this.uri = uri;
			this.call = call;
			this.callable = callable;
		}

		public String getUri() {
			return uri;
		}

		public IRemoteCall getRemoteCall() {
			return call;
		}

		public IRemoteCallable getRemoteCallable() {
			return callable;
		}
	}

	/**
	 * @param endpoint endpoint
	 * @param call call
	 * @param callable callable
	 * @return UriRequest new UriRequest
	 * @since 8.5
	 */
	protected UriRequest createUriRequest(String endpoint, IRemoteCall call, IRemoteCallable callable) {
		return getClientContainer().createUriRequest(endpoint, call, callable);
	}

	/**
	 * Invoke remote call.  The implementation of this method should actually 
	 * make the remote call for the given call and associated callable.
	 * 
	 * @param call the call for the remote call.  Will not be <code>null</code>.
	 * @param callable the callable associated with the remote call.  Will not be <code>null</code>.
	 * @return Object the result of the remote call.  May be <code>null</code>.
	 * @throws ECFException thrown if the call fails.
	 */
	protected abstract Object invokeRemoteCall(final IRemoteCall call, final IRemoteCallable callable) throws ECFException;

}
